/*!
 * # Semantic UI - Dropdown
 * http://github.com/semantic-org/semantic-ui/
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

;(function ($, window, document, undefined) {

  'use strict';
  
  window = (typeof window != 'undefined' && window.Math == Math)
    ? window
    : (typeof self != 'undefined' && self.Math == Math)
      ? self
      : Function('return this')()
  ;
  
  $.fn.dropdown = function(parameters) {
    var
      $allModules    = $(this),
      $document      = $(document),
  
      moduleSelector = $allModules.selector || '',
  
      hasTouch       = ('ontouchstart' in document.documentElement),
      time           = new Date().getTime(),
      performance    = [],
  
      query          = arguments[0],
      methodInvoked  = (typeof query == 'string'),
      queryArguments = [].slice.call(arguments, 1),
      returnedValue
    ;
  
    $allModules
      .each(function(elementIndex) {
        var
          settings          = ( $.isPlainObject(parameters) )
            ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
            : $.extend({}, $.fn.dropdown.settings),
  
          className       = settings.className,
          message         = settings.message,
          fields          = settings.fields,
          keys            = settings.keys,
          metadata        = settings.metadata,
          namespace       = settings.namespace,
          regExp          = settings.regExp,
          selector        = settings.selector,
          error           = settings.error,
          templates       = settings.templates,
  
          eventNamespace  = '.' + namespace,
          moduleNamespace = 'module-' + namespace,
  
          $module         = $(this),
          $context        = $(settings.context),
          $text           = $module.find(selector.text),
          $search         = $module.find(selector.search),
          $sizer          = $module.find(selector.sizer),
          $input          = $module.find(selector.input),
          $icon           = $module.find(selector.icon),
  
          $combo = ($module.prev().find(selector.text).length > 0)
            ? $module.prev().find(selector.text)
            : $module.prev(),
  
          $menu           = $module.children(selector.menu),
          $item           = $menu.find(selector.item),
  
          activated       = false,
          itemActivated   = false,
          internalChange  = false,
          element         = this,
          instance        = $module.data(moduleNamespace),
  
          initialLoad,
          pageLostFocus,
          willRefocus,
          elementNamespace,
          id,
          selectObserver,
          menuObserver,
          module
        ;
  
        module = {
  
          initialize: function() {
            module.debug('Initializing dropdown', settings);
  
            if( module.is.alreadySetup() ) {
              module.setup.reference();
            }
            else {
  
              module.setup.layout();
  
              if(settings.values) {
                module.change.values(settings.values);
              }
  
              module.refreshData();
  
              module.save.defaults();
              module.restore.selected();
  
              module.create.id();
              module.bind.events();
  
              module.observeChanges();
              module.instantiate();
            }
  
          },
  
          instantiate: function() {
            module.verbose('Storing instance of dropdown', module);
            instance = module;
            $module
              .data(moduleNamespace, module)
            ;
          },
  
          destroy: function() {
            module.verbose('Destroying previous dropdown', $module);
            module.remove.tabbable();
            $module
              .off(eventNamespace)
              .removeData(moduleNamespace)
            ;
            $menu
              .off(eventNamespace)
            ;
            $document
              .off(elementNamespace)
            ;
            module.disconnect.menuObserver();
            module.disconnect.selectObserver();
          },
  
          observeChanges: function() {
            if('MutationObserver' in window) {
              selectObserver = new MutationObserver(module.event.select.mutation);
              menuObserver   = new MutationObserver(module.event.menu.mutation);
              module.debug('Setting up mutation observer', selectObserver, menuObserver);
              module.observe.select();
              module.observe.menu();
            }
          },
  
          disconnect: {
            menuObserver: function() {
              if(menuObserver) {
                menuObserver.disconnect();
              }
            },
            selectObserver: function() {
              if(selectObserver) {
                selectObserver.disconnect();
              }
            }
          },
          observe: {
            select: function() {
              if(module.has.input()) {
                selectObserver.observe($module[0], {
                  childList : true,
                  subtree   : true
                });
              }
            },
            menu: function() {
              if(module.has.menu()) {
                menuObserver.observe($menu[0], {
                  childList : true,
                  subtree   : true
                });
              }
            }
          },
  
          create: {
            id: function() {
              id = (Math.random().toString(16) + '000000000').substr(2, 8);
              elementNamespace = '.' + id;
              module.verbose('Creating unique id for element', id);
            },
            userChoice: function(values) {
              var
                $userChoices,
                $userChoice,
                isUserValue,
                html
              ;
              values = values || module.get.userValues();
              if(!values) {
                return false;
              }
              values = $.isArray(values)
                ? values
                : [values]
              ;
              $.each(values, function(index, value) {
                if(module.get.item(value) === false) {
                  html         = settings.templates.addition( module.add.variables(message.addResult, value) );
                  $userChoice  = $('<div />')
                    .html(html)
                    .attr('data-' + metadata.value, value)
                    .attr('data-' + metadata.text, value)
                    .addClass(className.addition)
                    .addClass(className.item)
                  ;
                  if(settings.hideAdditions) {
                    $userChoice.addClass(className.hidden);
                  }
                  $userChoices = ($userChoices === undefined)
                    ? $userChoice
                    : $userChoices.add($userChoice)
                  ;
                  module.verbose('Creating user choices for value', value, $userChoice);
                }
              });
              return $userChoices;
            },
            userLabels: function(value) {
              var
                userValues = module.get.userValues()
              ;
              if(userValues) {
                module.debug('Adding user labels', userValues);
                $.each(userValues, function(index, value) {
                  module.verbose('Adding custom user value');
                  module.add.label(value, value);
                });
              }
            },
            menu: function() {
              $menu = $('<div />')
                .addClass(className.menu)
                .appendTo($module)
              ;
            },
            sizer: function() {
              $sizer = $('<span />')
                .addClass(className.sizer)
                .insertAfter($search)
              ;
            }
          },
  
          search: function(query) {
            query = (query !== undefined)
              ? query
              : module.get.query()
            ;
            module.verbose('Searching for query', query);
            if(module.has.minCharacters(query)) {
              module.filter(query);
            }
            else {
              module.hide();
            }
          },
  
          select: {
            firstUnfiltered: function() {
              module.verbose('Selecting first non-filtered element');
              module.remove.selectedItem();
              $item
                .not(selector.unselectable)
                .not(selector.addition + selector.hidden)
                  .eq(0)
                  .addClass(className.selected)
              ;
            },
            nextAvailable: function($selected) {
              $selected = $selected.eq(0);
              var
                $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
                $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
                hasNext        = ($nextAvailable.length > 0)
              ;
              if(hasNext) {
                module.verbose('Moving selection to', $nextAvailable);
                $nextAvailable.addClass(className.selected);
              }
              else {
                module.verbose('Moving selection to', $prevAvailable);
                $prevAvailable.addClass(className.selected);
              }
            }
          },
  
          setup: {
            api: function() {
              var
                apiSettings = {
                  debug   : settings.debug,
                  urlData : {
                    value : module.get.value(),
                    query : module.get.query()
                  },
                  on    : false
                }
              ;
              module.verbose('First request, initializing API');
              $module
                .api(apiSettings)
              ;
            },
            layout: function() {
              if( $module.is('select') ) {
                module.setup.select();
                module.setup.returnedObject();
              }
              if( !module.has.menu() ) {
                module.create.menu();
              }
              if( module.is.search() && !module.has.search() ) {
                module.verbose('Adding search input');
                $search = $('<input />')
                  .addClass(className.search)
                  .prop('autocomplete', 'off')
                  .insertBefore($text)
                ;
              }
              if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
                module.create.sizer();
              }
              if(settings.allowTab) {
                module.set.tabbable();
              }
            },
            select: function() {
              var
                selectValues  = module.get.selectValues()
              ;
              module.debug('Dropdown initialized on a select', selectValues);
              if( $module.is('select') ) {
                $input = $module;
              }
              // see if select is placed correctly already
              if($input.parent(selector.dropdown).length > 0) {
                module.debug('UI dropdown already exists. Creating dropdown menu only');
                $module = $input.closest(selector.dropdown);
                if( !module.has.menu() ) {
                  module.create.menu();
                }
                $menu = $module.children(selector.menu);
                module.setup.menu(selectValues);
              }
              else {
                module.debug('Creating entire dropdown from select');
                $module = $('<div />')
                  .attr('class', $input.attr('class') )
                  .addClass(className.selection)
                  .addClass(className.dropdown)
                  .html( templates.dropdown(selectValues) )
                  .insertBefore($input)
                ;
                if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
                  module.error(error.missingMultiple);
                  $input.prop('multiple', true);
                }
                if($input.is('[multiple]')) {
                  module.set.multiple();
                }
                if ($input.prop('disabled')) {
                  module.debug('Disabling dropdown');
                  $module.addClass(className.disabled);
                }
                $input
                  .removeAttr('class')
                  .detach()
                  .prependTo($module)
                ;
              }
              module.refresh();
            },
            menu: function(values) {
              $menu.html( templates.menu(values, fields));
              $item = $menu.find(selector.item);
            },
            reference: function() {
              module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
              // replace module reference
              $module  = $module.parent(selector.dropdown);
              instance = $module.data(moduleNamespace);
              element  = $module.get(0);
              module.refresh();
              module.setup.returnedObject();
            },
            returnedObject: function() {
              var
                $firstModules = $allModules.slice(0, elementIndex),
                $lastModules  = $allModules.slice(elementIndex + 1)
              ;
              // adjust all modules to use correct reference
              $allModules = $firstModules.add($module).add($lastModules);
            }
          },
  
          refresh: function() {
            module.refreshSelectors();
            module.refreshData();
          },
  
          refreshItems: function() {
            $item = $menu.find(selector.item);
          },
  
          refreshSelectors: function() {
            module.verbose('Refreshing selector cache');
            $text   = $module.find(selector.text);
            $search = $module.find(selector.search);
            $input  = $module.find(selector.input);
            $icon   = $module.find(selector.icon);
            $combo  = ($module.prev().find(selector.text).length > 0)
              ? $module.prev().find(selector.text)
              : $module.prev()
            ;
            $menu    = $module.children(selector.menu);
            $item    = $menu.find(selector.item);
          },
  
          refreshData: function() {
            module.verbose('Refreshing cached metadata');
            $item
              .removeData(metadata.text)
              .removeData(metadata.value)
            ;
          },
  
          clearData: function() {
            module.verbose('Clearing metadata');
            $item
              .removeData(metadata.text)
              .removeData(metadata.value)
            ;
            $module
              .removeData(metadata.defaultText)
              .removeData(metadata.defaultValue)
              .removeData(metadata.placeholderText)
            ;
          },
  
          toggle: function() {
            module.verbose('Toggling menu visibility');
            if( !module.is.active() ) {
              module.show();
            }
            else {
              module.hide();
            }
          },
  
          show: function(callback) {
            callback = $.isFunction(callback)
              ? callback
              : function(){}
            ;
            if(!module.can.show() && module.is.remote()) {
              module.debug('No API results retrieved, searching before show');
              module.queryRemote(module.get.query(), module.show);
            }
            if( module.can.show() && !module.is.active() ) {
              module.debug('Showing dropdown');
              if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
                module.remove.message();
              }
              if(module.is.allFiltered()) {
                return true;
              }
              if(settings.onShow.call(element) !== false) {
                module.animate.show(function() {
                  if( module.can.click() ) {
                    module.bind.intent();
                  }
                  if(module.has.menuSearch()) {
                    module.focusSearch();
                  }
                  module.set.visible();
                  callback.call(element);
                });
              }
            }
          },
  
          hide: function(callback) {
            callback = $.isFunction(callback)
              ? callback
              : function(){}
            ;
            if( module.is.active() && !module.is.animatingOutward() ) {
              module.debug('Hiding dropdown');
              if(settings.onHide.call(element) !== false) {
                module.animate.hide(function() {
                  module.remove.visible();
                  callback.call(element);
                });
              }
            }
          },
  
          hideOthers: function() {
            module.verbose('Finding other dropdowns to hide');
            $allModules
              .not($module)
                .has(selector.menu + '.' + className.visible)
                  .dropdown('hide')
            ;
          },
  
          hideMenu: function() {
            module.verbose('Hiding menu  instantaneously');
            module.remove.active();
            module.remove.visible();
            $menu.transition('hide');
          },
  
          hideSubMenus: function() {
            var
              $subMenus = $menu.children(selector.item).find(selector.menu)
            ;
            module.verbose('Hiding sub menus', $subMenus);
            $subMenus.transition('hide');
          },
  
          bind: {
            events: function() {
              if(hasTouch) {
                module.bind.touchEvents();
              }
              module.bind.keyboardEvents();
              module.bind.inputEvents();
              module.bind.mouseEvents();
            },
            touchEvents: function() {
              module.debug('Touch device detected binding additional touch events');
              if( module.is.searchSelection() ) {
                // do nothing special yet
              }
              else if( module.is.single() ) {
                $module
                  .on('touchstart' + eventNamespace, module.event.test.toggle)
                ;
              }
              $menu
                .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
              ;
            },
            keyboardEvents: function() {
              module.verbose('Binding keyboard events');
              $module
                .on('keydown' + eventNamespace, module.event.keydown)
              ;
              if( module.has.search() ) {
                $module
                  .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
                ;
              }
              if( module.is.multiple() ) {
                $document
                  .on('keydown' + elementNamespace, module.event.document.keydown)
                ;
              }
            },
            inputEvents: function() {
              module.verbose('Binding input change events');
              $module
                .on('change' + eventNamespace, selector.input, module.event.change)
              ;
            },
            mouseEvents: function() {
              module.verbose('Binding mouse events');
              if(module.is.multiple()) {
                $module
                  .on('click'   + eventNamespace, selector.label,  module.event.label.click)
                  .on('click'   + eventNamespace, selector.remove, module.event.remove.click)
                ;
              }
              if( module.is.searchSelection() ) {
                $module
                  .on('mousedown' + eventNamespace, module.event.mousedown)
                  .on('mouseup'   + eventNamespace, module.event.mouseup)
                  .on('mousedown' + eventNamespace, selector.menu,   module.event.menu.mousedown)
                  .on('mouseup'   + eventNamespace, selector.menu,   module.event.menu.mouseup)
                  .on('click'     + eventNamespace, selector.icon,   module.event.icon.click)
                  .on('focus'     + eventNamespace, selector.search, module.event.search.focus)
                  .on('click'     + eventNamespace, selector.search, module.event.search.focus)
                  .on('blur'      + eventNamespace, selector.search, module.event.search.blur)
                  .on('click'     + eventNamespace, selector.text,   module.event.text.focus)
                ;
                if(module.is.multiple()) {
                  $module
                    .on('click' + eventNamespace, module.event.click)
                  ;
                }
              }
              else {
                if(settings.on == 'click') {
                  $module
                    .on('click' + eventNamespace, module.event.test.toggle)
                  ;
                }
                else if(settings.on == 'hover') {
                  $module
                    .on('mouseenter' + eventNamespace, module.delay.show)
                    .on('mouseleave' + eventNamespace, module.delay.hide)
                  ;
                }
                else {
                  $module
                    .on(settings.on + eventNamespace, module.toggle)
                  ;
                }
                $module
                  .on('click' + eventNamespace, selector.icon, module.event.icon.click)
                  .on('mousedown' + eventNamespace, module.event.mousedown)
                  .on('mouseup'   + eventNamespace, module.event.mouseup)
                  .on('focus'     + eventNamespace, module.event.focus)
                ;
                if(module.has.menuSearch() ) {
                  $module
                    .on('blur' + eventNamespace, selector.search, module.event.search.blur)
                  ;
                }
                else {
                  $module
                    .on('blur' + eventNamespace, module.event.blur)
                  ;
                }
              }
              $menu
                .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter)
                .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
                .on('click'      + eventNamespace, selector.item, module.event.item.click)
              ;
            },
            intent: function() {
              module.verbose('Binding hide intent event to document');
              if(hasTouch) {
                $document
                  .on('touchstart' + elementNamespace, module.event.test.touch)
                  .on('touchmove'  + elementNamespace, module.event.test.touch)
                ;
              }
              $document
                .on('click' + elementNamespace, module.event.test.hide)
              ;
            }
          },
  
          unbind: {
            intent: function() {
              module.verbose('Removing hide intent event from document');
              if(hasTouch) {
                $document
                  .off('touchstart' + elementNamespace)
                  .off('touchmove' + elementNamespace)
                ;
              }
              $document
                .off('click' + elementNamespace)
              ;
            }
          },
  
          filter: function(query) {
            var
              searchTerm = (query !== undefined)
                ? query
                : module.get.query(),
              afterFiltered = function() {
                if(module.is.multiple()) {
                  module.filterActive();
                }
                if(query || (!query && module.get.activeItem().length == 0)) {
                  module.select.firstUnfiltered();
                }
                if( module.has.allResultsFiltered() ) {
                  if( settings.onNoResults.call(element, searchTerm) ) {
                    if(settings.allowAdditions) {
                      if(settings.hideAdditions) {
                        module.verbose('User addition with no menu, setting empty style');
                        module.set.empty();
                        module.hideMenu();
                      }
                    }
                    else {
                      module.verbose('All items filtered, showing message', searchTerm);
                      module.add.message(message.noResults);
                    }
                  }
                  else {
                    module.verbose('All items filtered, hiding dropdown', searchTerm);
                    module.hideMenu();
                  }
                }
                else {
                  module.remove.empty();
                  module.remove.message();
                }
                if(settings.allowAdditions) {
                  module.add.userSuggestion(query);
                }
                if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
                  module.show();
                }
              }
            ;
            if(settings.useLabels && module.has.maxSelections()) {
              return;
            }
            if(settings.apiSettings) {
              if( module.can.useAPI() ) {
                module.queryRemote(searchTerm, function() {
                  if(settings.filterRemoteData) {
                    module.filterItems(searchTerm);
                  }
                  afterFiltered();
                });
              }
              else {
                module.error(error.noAPI);
              }
            }
            else {
              module.filterItems(searchTerm);
              afterFiltered();
            }
          },
  
          queryRemote: function(query, callback) {
            var
              apiSettings = {
                errorDuration : false,
                cache         : 'local',
                throttle      : settings.throttle,
                urlData       : {
                  query: query
                },
                onError: function() {
                  module.add.message(message.serverError);
                  callback();
                },
                onFailure: function() {
                  module.add.message(message.serverError);
                  callback();
                },
                onSuccess : function(response) {
                  var
                    values          = response[fields.remoteValues],
                    hasRemoteValues = ($.isArray(values) && values.length > 0)
                  ;
                  if(hasRemoteValues) {
                    module.remove.message();
                    module.setup.menu({
                      values: response[fields.remoteValues]
                    });
                  }
                  else {
                    module.add.message(message.noResults);
                  }
                  callback();
                }
              }
            ;
            if( !$module.api('get request') ) {
              module.setup.api();
            }
            apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
            $module
              .api('setting', apiSettings)
              .api('query')
            ;
          },
  
          filterItems: function(query) {
            var
              searchTerm = (query !== undefined)
                ? query
                : module.get.query(),
              results          =  null,
              escapedTerm      = module.escape.string(searchTerm),
              beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm')
            ;
            // avoid loop if we're matching nothing
            if( module.has.query() ) {
              results = [];
  
              module.verbose('Searching for matching values', searchTerm);
              $item
                .each(function(){
                  var
                    $choice = $(this),
                    text,
                    value
                  ;
                  if(settings.match == 'both' || settings.match == 'text') {
                    text = String(module.get.choiceText($choice, false));
                    if(text.search(beginsWithRegExp) !== -1) {
                      results.push(this);
                      return true;
                    }
                    else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
                      results.push(this);
                      return true;
                    }
                    else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
                      results.push(this);
                      return true;
                    }
                  }
                  if(settings.match == 'both' || settings.match == 'value') {
                    value = String(module.get.choiceValue($choice, text));
                    if(value.search(beginsWithRegExp) !== -1) {
                      results.push(this);
                      return true;
                    }
                    else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
                      results.push(this);
                      return true;
                    }
                    else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
                      results.push(this);
                      return true;
                    }
                  }
                })
              ;
            }
            module.debug('Showing only matched items', searchTerm);
            module.remove.filteredItem();
            if(results) {
              $item
                .not(results)
                .addClass(className.filtered)
              ;
            }
          },
  
          fuzzySearch: function(query, term) {
            var
              termLength  = term.length,
              queryLength = query.length
            ;
            query = query.toLowerCase();
            term  = term.toLowerCase();
            if(queryLength > termLength) {
              return false;
            }
            if(queryLength === termLength) {
              return (query === term);
            }
            search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
              var
                queryCharacter = query.charCodeAt(characterIndex)
              ;
              while(nextCharacterIndex < termLength) {
                if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
                  continue search;
                }
              }
              return false;
            }
            return true;
          },
          exactSearch: function (query, term) {
            query = query.toLowerCase();
            term  = term.toLowerCase();
            if(term.indexOf(query) > -1) {
               return true;
            }
            return false;
          },
          filterActive: function() {
            if(settings.useLabels) {
              $item.filter('.' + className.active)
                .addClass(className.filtered)
              ;
            }
          },
  
          focusSearch: function(skipHandler) {
            if( module.has.search() && !module.is.focusedOnSearch() ) {
              if(skipHandler) {
                $module.off('focus' + eventNamespace, selector.search);
                $search.focus();
                $module.on('focus'  + eventNamespace, selector.search, module.event.search.focus);
              }
              else {
                $search.focus();
              }
            }
          },
  
          forceSelection: function() {
            var
              $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
              $activeItem        = $item.not(className.filtered).filter('.' + className.active).eq(0),
              $selectedItem      = ($currentlySelected.length > 0)
                ? $currentlySelected
                : $activeItem,
              hasSelected = ($selectedItem.length > 0)
            ;
            if(hasSelected && !module.is.multiple()) {
              module.debug('Forcing partial selection to selected item', $selectedItem);
              module.event.item.click.call($selectedItem, {}, true);
              return;
            }
            else {
              if(settings.allowAdditions) {
                module.set.selected(module.get.query());
                module.remove.searchTerm();
              }
              else {
                module.remove.searchTerm();
              }
            }
          },
  
          change: {
            values: function(values) {
              if(!settings.allowAdditions) {
                module.clear();
              }
              module.debug('Creating dropdown with specified values', values);
              module.setup.menu({values: values});
              $.each(values, function(index, item) {
                if(item.selected == true) {
                  module.debug('Setting initial selection to', item.value);
                  module.set.selected(item.value);
                  return true;
                }
              });
            }
          },
  
          event: {
            change: function() {
              if(!internalChange) {
                module.debug('Input changed, updating selection');
                module.set.selected();
              }
            },
            focus: function() {
              if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
                module.show();
              }
            },
            blur: function(event) {
              pageLostFocus = (document.activeElement === this);
              if(!activated && !pageLostFocus) {
                module.remove.activeLabel();
                module.hide();
              }
            },
            mousedown: function() {
              if(module.is.searchSelection()) {
                // prevent menu hiding on immediate re-focus
                willRefocus = true;
              }
              else {
                // prevents focus callback from occurring on mousedown
                activated = true;
              }
            },
            mouseup: function() {
              if(module.is.searchSelection()) {
                // prevent menu hiding on immediate re-focus
                willRefocus = false;
              }
              else {
                activated = false;
              }
            },
            click: function(event) {
              var
                $target = $(event.target)
              ;
              // focus search
              if($target.is($module)) {
                if(!module.is.focusedOnSearch()) {
                  module.focusSearch();
                }
                else {
                  module.show();
                }
              }
            },
            search: {
              focus: function() {
                activated = true;
                if(module.is.multiple()) {
                  module.remove.activeLabel();
                }
                if(settings.showOnFocus) {
                  module.search();
                }
              },
              blur: function(event) {
                pageLostFocus = (document.activeElement === this);
                if(module.is.searchSelection() && !willRefocus) {
                  if(!itemActivated && !pageLostFocus) {
                    if(settings.forceSelection) {
                      module.forceSelection();
                    }
                    module.hide();
                  }
                }
                willRefocus = false;
              }
            },
            icon: {
              click: function(event) {
                if($icon.hasClass(className.clear)) {
                  module.clear();
                }
                else if (module.can.click()) {
                  module.toggle();
                }
              }
            },
            text: {
              focus: function(event) {
                activated = true;
                module.focusSearch();
              }
            },
            input: function(event) {
              if(module.is.multiple() || module.is.searchSelection()) {
                module.set.filtered();
              }
              clearTimeout(module.timer);
              module.timer = setTimeout(module.search, settings.delay.search);
            },
            label: {
              click: function(event) {
                var
                  $label        = $(this),
                  $labels       = $module.find(selector.label),
                  $activeLabels = $labels.filter('.' + className.active),
                  $nextActive   = $label.nextAll('.' + className.active),
                  $prevActive   = $label.prevAll('.' + className.active),
                  $range = ($nextActive.length > 0)
                    ? $label.nextUntil($nextActive).add($activeLabels).add($label)
                    : $label.prevUntil($prevActive).add($activeLabels).add($label)
                ;
                if(event.shiftKey) {
                  $activeLabels.removeClass(className.active);
                  $range.addClass(className.active);
                }
                else if(event.ctrlKey) {
                  $label.toggleClass(className.active);
                }
                else {
                  $activeLabels.removeClass(className.active);
                  $label.addClass(className.active);
                }
                settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
              }
            },
            remove: {
              click: function() {
                var
                  $label = $(this).parent()
                ;
                if( $label.hasClass(className.active) ) {
                  // remove all selected labels
                  module.remove.activeLabels();
                }
                else {
                  // remove this label only
                  module.remove.activeLabels( $label );
                }
              }
            },
            test: {
              toggle: function(event) {
                var
                  toggleBehavior = (module.is.multiple())
                    ? module.show
                    : module.toggle
                ;
                if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
                  return;
                }
                if( module.determine.eventOnElement(event, toggleBehavior) ) {
                  event.preventDefault();
                }
              },
              touch: function(event) {
                module.determine.eventOnElement(event, function() {
                  if(event.type == 'touchstart') {
                    module.timer = setTimeout(function() {
                      module.hide();
                    }, settings.delay.touch);
                  }
                  else if(event.type == 'touchmove') {
                    clearTimeout(module.timer);
                  }
                });
                event.stopPropagation();
              },
              hide: function(event) {
                module.determine.eventInModule(event, module.hide);
              }
            },
            select: {
              mutation: function(mutations) {
                module.debug('<select> modified, recreating menu');
                var
                  isSelectMutation = false
                ;
                $.each(mutations, function(index, mutation) {
                  if($(mutation.target).is('select') || $(mutation.addedNodes).is('select')) {
                    isSelectMutation = true;
                    return true;
                  }
                });
                if(isSelectMutation) {
                  module.disconnect.selectObserver();
                  module.refresh();
                  module.setup.select();
                  module.set.selected();
                  module.observe.select();
                }
              }
            },
            menu: {
              mutation: function(mutations) {
                var
                  mutation   = mutations[0],
                  $addedNode = mutation.addedNodes
                    ? $(mutation.addedNodes[0])
                    : $(false),
                  $removedNode = mutation.removedNodes
                    ? $(mutation.removedNodes[0])
                    : $(false),
                  $changedNodes  = $addedNode.add($removedNode),
                  isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0,
                  isMessage      = $changedNodes.is(selector.message)  || $changedNodes.closest(selector.message).length > 0
                ;
                if(isUserAddition || isMessage) {
                  module.debug('Updating item selector cache');
                  module.refreshItems();
                }
                else {
                  module.debug('Menu modified, updating selector cache');
                  module.refresh();
                }
              },
              mousedown: function() {
                itemActivated = true;
              },
              mouseup: function() {
                itemActivated = false;
              }
            },
            item: {
              mouseenter: function(event) {
                var
                  $target        = $(event.target),
                  $item          = $(this),
                  $subMenu       = $item.children(selector.menu),
                  $otherMenus    = $item.siblings(selector.item).children(selector.menu),
                  hasSubMenu     = ($subMenu.length > 0),
                  isBubbledEvent = ($subMenu.find($target).length > 0)
                ;
                if( !isBubbledEvent && hasSubMenu ) {
                  clearTimeout(module.itemTimer);
                  module.itemTimer = setTimeout(function() {
                    module.verbose('Showing sub-menu', $subMenu);
                    $.each($otherMenus, function() {
                      module.animate.hide(false, $(this));
                    });
                    module.animate.show(false, $subMenu);
                  }, settings.delay.show);
                  event.preventDefault();
                }
              },
              mouseleave: function(event) {
                var
                  $subMenu = $(this).children(selector.menu)
                ;
                if($subMenu.length > 0) {
                  clearTimeout(module.itemTimer);
                  module.itemTimer = setTimeout(function() {
                    module.verbose('Hiding sub-menu', $subMenu);
                    module.animate.hide(false, $subMenu);
                  }, settings.delay.hide);
                }
              },
              click: function (event, skipRefocus) {
                var
                  $choice        = $(this),
                  $target        = (event)
                    ? $(event.target)
                    : $(''),
                  $subMenu       = $choice.find(selector.menu),
                  text           = module.get.choiceText($choice),
                  value          = module.get.choiceValue($choice, text),
                  hasSubMenu     = ($subMenu.length > 0),
                  isBubbledEvent = ($subMenu.find($target).length > 0)
                ;
                // prevents IE11 bug where menu receives focus even though `tabindex=-1`
                if(module.has.menuSearch()) {
                  $(document.activeElement).blur();
                }
                if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) {
                  if(module.is.searchSelection()) {
                    if(settings.allowAdditions) {
                      module.remove.userAddition();
                    }
                    module.remove.searchTerm();
                    if(!module.is.focusedOnSearch() && !(skipRefocus == true)) {
                      module.focusSearch(true);
                    }
                  }
                  if(!settings.useLabels) {
                    module.remove.filteredItem();
                    module.set.scrollPosition($choice);
                  }
                  module.determine.selectAction.call(this, text, value);
                }
              }
            },
  
            document: {
              // label selection should occur even when element has no focus
              keydown: function(event) {
                var
                  pressedKey    = event.which,
                  isShortcutKey = module.is.inObject(pressedKey, keys)
                ;
                if(isShortcutKey) {
                  var
                    $label            = $module.find(selector.label),
                    $activeLabel      = $label.filter('.' + className.active),
                    activeValue       = $activeLabel.data(metadata.value),
                    labelIndex        = $label.index($activeLabel),
                    labelCount        = $label.length,
                    hasActiveLabel    = ($activeLabel.length > 0),
                    hasMultipleActive = ($activeLabel.length > 1),
                    isFirstLabel      = (labelIndex === 0),
                    isLastLabel       = (labelIndex + 1 == labelCount),
                    isSearch          = module.is.searchSelection(),
                    isFocusedOnSearch = module.is.focusedOnSearch(),
                    isFocused         = module.is.focused(),
                    caretAtStart      = (isFocusedOnSearch && module.get.caretPosition() === 0),
                    $nextLabel
                  ;
                  if(isSearch && !hasActiveLabel && !isFocusedOnSearch) {
                    return;
                  }
  
                  if(pressedKey == keys.leftArrow) {
                    // activate previous label
                    if((isFocused || caretAtStart) && !hasActiveLabel) {
                      module.verbose('Selecting previous label');
                      $label.last().addClass(className.active);
                    }
                    else if(hasActiveLabel) {
                      if(!event.shiftKey) {
                        module.verbose('Selecting previous label');
                        $label.removeClass(className.active);
                      }
                      else {
                        module.verbose('Adding previous label to selection');
                      }
                      if(isFirstLabel && !hasMultipleActive) {
                        $activeLabel.addClass(className.active);
                      }
                      else {
                        $activeLabel.prev(selector.siblingLabel)
                          .addClass(className.active)
                          .end()
                        ;
                      }
                      event.preventDefault();
                    }
                  }
                  else if(pressedKey == keys.rightArrow) {
                    // activate first label
                    if(isFocused && !hasActiveLabel) {
                      $label.first().addClass(className.active);
                    }
                    // activate next label
                    if(hasActiveLabel) {
                      if(!event.shiftKey) {
                        module.verbose('Selecting next label');
                        $label.removeClass(className.active);
                      }
                      else {
                        module.verbose('Adding next label to selection');
                      }
                      if(isLastLabel) {
                        if(isSearch) {
                          if(!isFocusedOnSearch) {
                            module.focusSearch();
                          }
                          else {
                            $label.removeClass(className.active);
                          }
                        }
                        else if(hasMultipleActive) {
                          $activeLabel.next(selector.siblingLabel).addClass(className.active);
                        }
                        else {
                          $activeLabel.addClass(className.active);
                        }
                      }
                      else {
                        $activeLabel.next(selector.siblingLabel).addClass(className.active);
                      }
                      event.preventDefault();
                    }
                  }
                  else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
                    if(hasActiveLabel) {
                      module.verbose('Removing active labels');
                      if(isLastLabel) {
                        if(isSearch && !isFocusedOnSearch) {
                          module.focusSearch();
                        }
                      }
                      $activeLabel.last().next(selector.siblingLabel).addClass(className.active);
                      module.remove.activeLabels($activeLabel);
                      event.preventDefault();
                    }
                    else if(caretAtStart && !hasActiveLabel && pressedKey == keys.backspace) {
                      module.verbose('Removing last label on input backspace');
                      $activeLabel = $label.last().addClass(className.active);
                      module.remove.activeLabels($activeLabel);
                    }
                  }
                  else {
                    $activeLabel.removeClass(className.active);
                  }
                }
              }
            },
  
            keydown: function(event) {
              var
                pressedKey    = event.which,
                isShortcutKey = module.is.inObject(pressedKey, keys)
              ;
              if(isShortcutKey) {
                var
                  $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
                  $activeItem        = $menu.children('.' + className.active).eq(0),
                  $selectedItem      = ($currentlySelected.length > 0)
                    ? $currentlySelected
                    : $activeItem,
                  $visibleItems = ($selectedItem.length > 0)
                    ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack()
                    : $menu.children(':not(.' + className.filtered +')'),
                  $subMenu              = $selectedItem.children(selector.menu),
                  $parentMenu           = $selectedItem.closest(selector.menu),
                  inVisibleMenu         = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0),
                  hasSubMenu            = ($subMenu.length> 0),
                  hasSelectedItem       = ($selectedItem.length > 0),
                  selectedIsSelectable  = ($selectedItem.not(selector.unselectable).length > 0),
                  delimiterPressed      = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()),
                  isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable),
                  $nextItem,
                  isSubMenuItem,
                  newIndex
                ;
                // allow selection with menu closed
                if(isAdditionWithoutMenu) {
                  module.verbose('Selecting item from keyboard shortcut', $selectedItem);
                  module.event.item.click.call($selectedItem, event);
                  if(module.is.searchSelection()) {
                    module.remove.searchTerm();
                  }
                }
  
                // visible menu keyboard shortcuts
                if( module.is.visible() ) {
  
                  // enter (select or open sub-menu)
                  if(pressedKey == keys.enter || delimiterPressed) {
                    if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) {
                      module.verbose('Pressed enter on unselectable category, opening sub menu');
                      pressedKey = keys.rightArrow;
                    }
                    else if(selectedIsSelectable) {
                      module.verbose('Selecting item from keyboard shortcut', $selectedItem);
                      module.event.item.click.call($selectedItem, event);
                      if(module.is.searchSelection()) {
                        module.remove.searchTerm();
                      }
                    }
                    event.preventDefault();
                  }
  
                  // sub-menu actions
                  if(hasSelectedItem) {
  
                    if(pressedKey == keys.leftArrow) {
  
                      isSubMenuItem = ($parentMenu[0] !== $menu[0]);
  
                      if(isSubMenuItem) {
                        module.verbose('Left key pressed, closing sub-menu');
                        module.animate.hide(false, $parentMenu);
                        $selectedItem
                          .removeClass(className.selected)
                        ;
                        $parentMenu
                          .closest(selector.item)
                            .addClass(className.selected)
                        ;
                        event.preventDefault();
                      }
                    }
  
                    // right arrow (show sub-menu)
                    if(pressedKey == keys.rightArrow) {
                      if(hasSubMenu) {
                        module.verbose('Right key pressed, opening sub-menu');
                        module.animate.show(false, $subMenu);
                        $selectedItem
                          .removeClass(className.selected)
                        ;
                        $subMenu
                          .find(selector.item).eq(0)
                            .addClass(className.selected)
                        ;
                        event.preventDefault();
                      }
                    }
                  }
  
                  // up arrow (traverse menu up)
                  if(pressedKey == keys.upArrow) {
                    $nextItem = (hasSelectedItem && inVisibleMenu)
                      ? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
                      : $item.eq(0)
                    ;
                    if($visibleItems.index( $nextItem ) < 0) {
                      module.verbose('Up key pressed but reached top of current menu');
                      event.preventDefault();
                      return;
                    }
                    else {
                      module.verbose('Up key pressed, changing active item');
                      $selectedItem
                        .removeClass(className.selected)
                      ;
                      $nextItem
                        .addClass(className.selected)
                      ;
                      module.set.scrollPosition($nextItem);
                      if(settings.selectOnKeydown && module.is.single()) {
                        module.set.selectedItem($nextItem);
                      }
                    }
                    event.preventDefault();
                  }
  
                  // down arrow (traverse menu down)
                  if(pressedKey == keys.downArrow) {
                    $nextItem = (hasSelectedItem && inVisibleMenu)
                      ? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
                      : $item.eq(0)
                    ;
                    if($nextItem.length === 0) {
                      module.verbose('Down key pressed but reached bottom of current menu');
                      event.preventDefault();
                      return;
                    }
                    else {
                      module.verbose('Down key pressed, changing active item');
                      $item
                        .removeClass(className.selected)
                      ;
                      $nextItem
                        .addClass(className.selected)
                      ;
                      module.set.scrollPosition($nextItem);
                      if(settings.selectOnKeydown && module.is.single()) {
                        module.set.selectedItem($nextItem);
                      }
                    }
                    event.preventDefault();
                  }
  
                  // page down (show next page)
                  if(pressedKey == keys.pageUp) {
                    module.scrollPage('up');
                    event.preventDefault();
                  }
                  if(pressedKey == keys.pageDown) {
                    module.scrollPage('down');
                    event.preventDefault();
                  }
  
                  // escape (close menu)
                  if(pressedKey == keys.escape) {
                    module.verbose('Escape key pressed, closing dropdown');
                    module.hide();
                  }
  
                }
                else {
                  // delimiter key
                  if(delimiterPressed) {
                    event.preventDefault();
                  }
                  // down arrow (open menu)
                  if(pressedKey == keys.downArrow && !module.is.visible()) {
                    module.verbose('Down key pressed, showing dropdown');
                    module.show();
                    event.preventDefault();
                  }
                }
              }
              else {
                if( !module.has.search() ) {
                  module.set.selectedLetter( String.fromCharCode(pressedKey) );
                }
              }
            }
          },
  
          trigger: {
            change: function() {
              var
                events       = document.createEvent('HTMLEvents'),
                inputElement = $input[0]
              ;
              if(inputElement) {
                module.verbose('Triggering native change event');
                events.initEvent('change', true, false);
                inputElement.dispatchEvent(events);
              }
            }
          },
  
          determine: {
            selectAction: function(text, value) {
              module.verbose('Determining action', settings.action);
              if( $.isFunction( module.action[settings.action] ) ) {
                module.verbose('Triggering preset action', settings.action, text, value);
                module.action[ settings.action ].call(element, text, value, this);
              }
              else if( $.isFunction(settings.action) ) {
                module.verbose('Triggering user action', settings.action, text, value);
                settings.action.call(element, text, value, this);
              }
              else {
                module.error(error.action, settings.action);
              }
            },
            eventInModule: function(event, callback) {
              var
                $target    = $(event.target),
                inDocument = ($target.closest(document.documentElement).length > 0),
                inModule   = ($target.closest($module).length > 0)
              ;
              callback = $.isFunction(callback)
                ? callback
                : function(){}
              ;
              if(inDocument && !inModule) {
                module.verbose('Triggering event', callback);
                callback();
                return true;
              }
              else {
                module.verbose('Event occurred in dropdown, canceling callback');
                return false;
              }
            },
            eventOnElement: function(event, callback) {
              var
                $target      = $(event.target),
                $label       = $target.closest(selector.siblingLabel),
                inVisibleDOM = document.body.contains(event.target),
                notOnLabel   = ($module.find($label).length === 0),
                notInMenu    = ($target.closest($menu).length === 0)
              ;
              callback = $.isFunction(callback)
                ? callback
                : function(){}
              ;
              if(inVisibleDOM && notOnLabel && notInMenu) {
                module.verbose('Triggering event', callback);
                callback();
                return true;
              }
              else {
                module.verbose('Event occurred in dropdown menu, canceling callback');
                return false;
              }
            }
          },
  
          action: {
  
            nothing: function() {},
  
            activate: function(text, value, element) {
              value = (value !== undefined)
                ? value
                : text
              ;
              if( module.can.activate( $(element) ) ) {
                module.set.selected(value, $(element));
                if(module.is.multiple() && !module.is.allFiltered()) {
                  return;
                }
                else {
                  module.hideAndClear();
                }
              }
            },
  
            select: function(text, value, element) {
              value = (value !== undefined)
                ? value
                : text
              ;
              if( module.can.activate( $(element) ) ) {
                module.set.value(value, text, $(element));
                if(module.is.multiple() && !module.is.allFiltered()) {
                  return;
                }
                else {
                  module.hideAndClear();
                }
              }
            },
  
            combo: function(text, value, element) {
              value = (value !== undefined)
                ? value
                : text
              ;
              module.set.selected(value, $(element));
              module.hideAndClear();
            },
  
            hide: function(text, value, element) {
              module.set.value(value, text, $(element));
              module.hideAndClear();
            }
  
          },
  
          get: {
            id: function() {
              return id;
            },
            defaultText: function() {
              return $module.data(metadata.defaultText);
            },
            defaultValue: function() {
              return $module.data(metadata.defaultValue);
            },
            placeholderText: function() {
              if(settings.placeholder != 'auto' && typeof settings.placeholder == 'string') {
                return settings.placeholder;
              }
              return $module.data(metadata.placeholderText) || '';
            },
            text: function() {
              return $text.text();
            },
            query: function() {
              return $.trim($search.val());
            },
            searchWidth: function(value) {
              value = (value !== undefined)
                ? value
                : $search.val()
              ;
              $sizer.text(value);
              // prevent rounding issues
              return Math.ceil( $sizer.width() + 1);
            },
            selectionCount: function() {
              var
                values = module.get.values(),
                count
              ;
              count = ( module.is.multiple() )
                ? $.isArray(values)
                  ? values.length
                  : 0
                : (module.get.value() !== '')
                  ? 1
                  : 0
              ;
              return count;
            },
            transition: function($subMenu) {
              return (settings.transition == 'auto')
                ? module.is.upward($subMenu)
                  ? 'slide up'
                  : 'slide down'
                : settings.transition
              ;
            },
            userValues: function() {
              var
                values = module.get.values()
              ;
              if(!values) {
                return false;
              }
              values = $.isArray(values)
                ? values
                : [values]
              ;
              return $.grep(values, function(value) {
                return (module.get.item(value) === false);
              });
            },
            uniqueArray: function(array) {
              return $.grep(array, function (value, index) {
                  return $.inArray(value, array) === index;
              });
            },
            caretPosition: function() {
              var
                input = $search.get(0),
                range,
                rangeLength
              ;
              if('selectionStart' in input) {
                return input.selectionStart;
              }
              else if (document.selection) {
                input.focus();
                range       = document.selection.createRange();
                rangeLength = range.text.length;
                range.moveStart('character', -input.value.length);
                return range.text.length - rangeLength;
              }
            },
            value: function() {
              var
                value = ($input.length > 0)
                  ? $input.val()
                  : $module.data(metadata.value),
                isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '')
              ;
              // prevents placeholder element from being selected when multiple
              return (value === undefined || isEmptyMultiselect)
                ? ''
                : value
              ;
            },
            values: function() {
              var
                value = module.get.value()
              ;
              if(value === '') {
                return '';
              }
              return ( !module.has.selectInput() && module.is.multiple() )
                ? (typeof value == 'string') // delimited string
                  ? value.split(settings.delimiter)
                  : ''
                : value
              ;
            },
            remoteValues: function() {
              var
                values = module.get.values(),
                remoteValues = false
              ;
              if(values) {
                if(typeof values == 'string') {
                  values = [values];
                }
                $.each(values, function(index, value) {
                  var
                    name = module.read.remoteData(value)
                  ;
                  module.verbose('Restoring value from session data', name, value);
                  if(name) {
                    if(!remoteValues) {
                      remoteValues = {};
                    }
                    remoteValues[value] = name;
                  }
                });
              }
              return remoteValues;
            },
            choiceText: function($choice, preserveHTML) {
              preserveHTML = (preserveHTML !== undefined)
                ? preserveHTML
                : settings.preserveHTML
              ;
              if($choice) {
                if($choice.find(selector.menu).length > 0) {
                  module.verbose('Retrieving text of element with sub-menu');
                  $choice = $choice.clone();
                  $choice.find(selector.menu).remove();
                  $choice.find(selector.menuIcon).remove();
                }
                return ($choice.data(metadata.text) !== undefined)
                  ? $choice.data(metadata.text)
                  : (preserveHTML)
                    ? $.trim($choice.html())
                    : $.trim($choice.text())
                ;
              }
            },
            choiceValue: function($choice, choiceText) {
              choiceText = choiceText || module.get.choiceText($choice);
              if(!$choice) {
                return false;
              }
              return ($choice.data(metadata.value) !== undefined)
                ? String( $choice.data(metadata.value) )
                : (typeof choiceText === 'string')
                  ? $.trim(choiceText.toLowerCase())
                  : String(choiceText)
              ;
            },
            inputEvent: function() {
              var
                input = $search[0]
              ;
              if(input) {
                return (input.oninput !== undefined)
                  ? 'input'
                  : (input.onpropertychange !== undefined)
                    ? 'propertychange'
                    : 'keyup'
                ;
              }
              return false;
            },
            selectValues: function() {
              var
                select = {}
              ;
              select.values = [];
              $module
                .find('option')
                  .each(function() {
                    var
                      $option  = $(this),
                      name     = $option.html(),
                      disabled = $option.attr('disabled'),
                      value    = ( $option.attr('value') !== undefined )
                        ? $option.attr('value')
                        : name
                    ;
                    if(settings.placeholder === 'auto' && value === '') {
                      select.placeholder = name;
                    }
                    else {
                      select.values.push({
                        name     : name,
                        value    : value,
                        disabled : disabled
                      });
                    }
                  })
              ;
              if(settings.placeholder && settings.placeholder !== 'auto') {
                module.debug('Setting placeholder value to', settings.placeholder);
                select.placeholder = settings.placeholder;
              }
              if(settings.sortSelect) {
                select.values.sort(function(a, b) {
                  return (a.name > b.name)
                    ? 1
                    : -1
                  ;
                });
                module.debug('Retrieved and sorted values from select', select);
              }
              else {
                module.debug('Retrieved values from select', select);
              }
              return select;
            },
            activeItem: function() {
              return $item.filter('.'  + className.active);
            },
            selectedItem: function() {
              var
                $selectedItem = $item.not(selector.unselectable).filter('.'  + className.selected)
              ;
              return ($selectedItem.length > 0)
                ? $selectedItem
                : $item.eq(0)
              ;
            },
            itemWithAdditions: function(value) {
              var
                $items       = module.get.item(value),
                $userItems   = module.create.userChoice(value),
                hasUserItems = ($userItems && $userItems.length > 0)
              ;
              if(hasUserItems) {
                $items = ($items.length > 0)
                  ? $items.add($userItems)
                  : $userItems
                ;
              }
              return $items;
            },
            item: function(value, strict) {
              var
                $selectedItem = false,
                shouldSearch,
                isMultiple
              ;
              value = (value !== undefined)
                ? value
                : ( module.get.values() !== undefined)
                  ? module.get.values()
                  : module.get.text()
              ;
              shouldSearch = (isMultiple)
                ? (value.length > 0)
                : (value !== undefined && value !== null)
              ;
              isMultiple = (module.is.multiple() && $.isArray(value));
              strict     = (value === '' || value === 0)
                ? true
                : strict || false
              ;
              if(shouldSearch) {
                $item
                  .each(function() {
                    var
                      $choice       = $(this),
                      optionText    = module.get.choiceText($choice),
                      optionValue   = module.get.choiceValue($choice, optionText)
                    ;
                    // safe early exit
                    if(optionValue === null || optionValue === undefined) {
                      return;
                    }
                    if(isMultiple) {
                      if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) {
                        $selectedItem = ($selectedItem)
                          ? $selectedItem.add($choice)
                          : $choice
                        ;
                      }
                    }
                    else if(strict) {
                      module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
                      if( optionValue === value || optionText === value) {
                        $selectedItem = $choice;
                        return true;
                      }
                    }
                    else {
                      if( String(optionValue) == String(value) || optionText == value) {
                        module.verbose('Found select item by value', optionValue, value);
                        $selectedItem = $choice;
                        return true;
                      }
                    }
                  })
                ;
              }
              return $selectedItem;
            }
          },
  
          check: {
            maxSelections: function(selectionCount) {
              if(settings.maxSelections) {
                selectionCount = (selectionCount !== undefined)
                  ? selectionCount
                  : module.get.selectionCount()
                ;
                if(selectionCount >= settings.maxSelections) {
                  module.debug('Maximum selection count reached');
                  if(settings.useLabels) {
                    $item.addClass(className.filtered);
                    module.add.message(message.maxSelections);
                  }
                  return true;
                }
                else {
                  module.verbose('No longer at maximum selection count');
                  module.remove.message();
                  module.remove.filteredItem();
                  if(module.is.searchSelection()) {
                    module.filterItems();
                  }
                  return false;
                }
              }
              return true;
            }
          },
  
          restore: {
            defaults: function() {
              module.clear();
              module.restore.defaultText();
              module.restore.defaultValue();
            },
            defaultText: function() {
              var
                defaultText     = module.get.defaultText(),
                placeholderText = module.get.placeholderText
              ;
              if(defaultText === placeholderText) {
                module.debug('Restoring default placeholder text', defaultText);
                module.set.placeholderText(defaultText);
              }
              else {
                module.debug('Restoring default text', defaultText);
                module.set.text(defaultText);
              }
            },
            placeholderText: function() {
              module.set.placeholderText();
            },
            defaultValue: function() {
              var
                defaultValue = module.get.defaultValue()
              ;
              if(defaultValue !== undefined) {
                module.debug('Restoring default value', defaultValue);
                if(defaultValue !== '') {
                  module.set.value(defaultValue);
                  module.set.selected();
                }
                else {
                  module.remove.activeItem();
                  module.remove.selectedItem();
                }
              }
            },
            labels: function() {
              if(settings.allowAdditions) {
                if(!settings.useLabels) {
                  module.error(error.labels);
                  settings.useLabels = true;
                }
                module.debug('Restoring selected values');
                module.create.userLabels();
              }
              module.check.maxSelections();
            },
            selected: function() {
              module.restore.values();
              if(module.is.multiple()) {
                module.debug('Restoring previously selected values and labels');
                module.restore.labels();
              }
              else {
                module.debug('Restoring previously selected values');
              }
            },
            values: function() {
              // prevents callbacks from occurring on initial load
              module.set.initialLoad();
              if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) {
                module.restore.remoteValues();
              }
              else {
                module.set.selected();
              }
              module.remove.initialLoad();
            },
            remoteValues: function() {
              var
                values = module.get.remoteValues()
              ;
              module.debug('Recreating selected from session data', values);
              if(values) {
                if( module.is.single() ) {
                  $.each(values, function(value, name) {
                    module.set.text(name);
                  });
                }
                else {
                  $.each(values, function(value, name) {
                    module.add.label(value, name);
                  });
                }
              }
            }
          },
  
          read: {
            remoteData: function(value) {
              var
                name
              ;
              if(window.Storage === undefined) {
                module.error(error.noStorage);
                return;
              }
              name = sessionStorage.getItem(value);
              return (name !== undefined)
                ? name
                : false
              ;
            }
          },
  
          save: {
            defaults: function() {
              module.save.defaultText();
              module.save.placeholderText();
              module.save.defaultValue();
            },
            defaultValue: function() {
              var
                value = module.get.value()
              ;
              module.verbose('Saving default value as', value);
              $module.data(metadata.defaultValue, value);
            },
            defaultText: function() {
              var
                text = module.get.text()
              ;
              module.verbose('Saving default text as', text);
              $module.data(metadata.defaultText, text);
            },
            placeholderText: function() {
              var
                text
              ;
              if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
                text = module.get.text();
                module.verbose('Saving placeholder text as', text);
                $module.data(metadata.placeholderText, text);
              }
            },
            remoteData: function(name, value) {
              if(window.Storage === undefined) {
                module.error(error.noStorage);
                return;
              }
              module.verbose('Saving remote data to session storage', value, name);
              sessionStorage.setItem(value, name);
            }
          },
  
          clear: function() {
            if(module.is.multiple() && settings.useLabels) {
              module.remove.labels();
            }
            else {
              module.remove.activeItem();
              module.remove.selectedItem();
            }
            module.set.placeholderText();
            module.clearValue();
          },
  
          clearValue: function() {
            module.set.value('');
          },
  
          scrollPage: function(direction, $selectedItem) {
            var
              $currentItem  = $selectedItem || module.get.selectedItem(),
              $menu         = $currentItem.closest(selector.menu),
              menuHeight    = $menu.outerHeight(),
              currentScroll = $menu.scrollTop(),
              itemHeight    = $item.eq(0).outerHeight(),
              itemsPerPage  = Math.floor(menuHeight / itemHeight),
              maxScroll     = $menu.prop('scrollHeight'),
              newScroll     = (direction == 'up')
                ? currentScroll - (itemHeight * itemsPerPage)
                : currentScroll + (itemHeight * itemsPerPage),
              $selectableItem = $item.not(selector.unselectable),
              isWithinRange,
              $nextSelectedItem,
              elementIndex
            ;
            elementIndex      = (direction == 'up')
              ? $selectableItem.index($currentItem) - itemsPerPage
              : $selectableItem.index($currentItem) + itemsPerPage
            ;
            isWithinRange = (direction == 'up')
              ? (elementIndex >= 0)
              : (elementIndex < $selectableItem.length)
            ;
            $nextSelectedItem = (isWithinRange)
              ? $selectableItem.eq(elementIndex)
              : (direction == 'up')
                ? $selectableItem.first()
                : $selectableItem.last()
            ;
            if($nextSelectedItem.length > 0) {
              module.debug('Scrolling page', direction, $nextSelectedItem);
              $currentItem
                .removeClass(className.selected)
              ;
              $nextSelectedItem
                .addClass(className.selected)
              ;
              if(settings.selectOnKeydown && module.is.single()) {
                module.set.selectedItem($nextSelectedItem);
              }
              $menu
                .scrollTop(newScroll)
              ;
            }
          },
  
          set: {
            filtered: function() {
              var
                isMultiple       = module.is.multiple(),
                isSearch         = module.is.searchSelection(),
                isSearchMultiple = (isMultiple && isSearch),
                searchValue      = (isSearch)
                  ? module.get.query()
                  : '',
                hasSearchValue   = (typeof searchValue === 'string' && searchValue.length > 0),
                searchWidth      = module.get.searchWidth(),
                valueIsSet       = searchValue !== ''
              ;
              if(isMultiple && hasSearchValue) {
                module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
                $search.css('width', searchWidth);
              }
              if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
                module.verbose('Hiding placeholder text');
                $text.addClass(className.filtered);
              }
              else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
                module.verbose('Showing placeholder text');
                $text.removeClass(className.filtered);
              }
            },
            empty: function() {
              $module.addClass(className.empty);
            },
            loading: function() {
              $module.addClass(className.loading);
            },
            placeholderText: function(text) {
              text = text || module.get.placeholderText();
              module.debug('Setting placeholder text', text);
              module.set.text(text);
              $text.addClass(className.placeholder);
            },
            tabbable: function() {
              if( module.is.searchSelection() ) {
                module.debug('Added tabindex to searchable dropdown');
                $search
                  .val('')
                  .attr('tabindex', 0)
                ;
                $menu
                  .attr('tabindex', -1)
                ;
              }
              else {
                module.debug('Added tabindex to dropdown');
                if( $module.attr('tabindex') === undefined) {
                  $module
                    .attr('tabindex', 0)
                  ;
                  $menu
                    .attr('tabindex', -1)
                  ;
                }
              }
            },
            initialLoad: function() {
              module.verbose('Setting initial load');
              initialLoad = true;
            },
            activeItem: function($item) {
              if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
                $item.addClass(className.filtered);
              }
              else {
                $item.addClass(className.active);
              }
            },
            partialSearch: function(text) {
              var
                length = module.get.query().length
              ;
              $search.val( text.substr(0, length));
            },
            scrollPosition: function($item, forceScroll) {
              var
                edgeTolerance = 5,
                $menu,
                hasActive,
                offset,
                itemHeight,
                itemOffset,
                menuOffset,
                menuScroll,
                menuHeight,
                abovePage,
                belowPage
              ;
  
              $item       = $item || module.get.selectedItem();
              $menu       = $item.closest(selector.menu);
              hasActive   = ($item && $item.length > 0);
              forceScroll = (forceScroll !== undefined)
                ? forceScroll
                : false
              ;
              if($item && $menu.length > 0 && hasActive) {
                itemOffset = $item.position().top;
  
                $menu.addClass(className.loading);
                menuScroll = $menu.scrollTop();
                menuOffset = $menu.offset().top;
                itemOffset = $item.offset().top;
                offset     = menuScroll - menuOffset + itemOffset;
                if(!forceScroll) {
                  menuHeight = $menu.height();
                  belowPage  = menuScroll + menuHeight < (offset + edgeTolerance);
                  abovePage  = ((offset - edgeTolerance) < menuScroll);
                }
                module.debug('Scrolling to active item', offset);
                if(forceScroll || abovePage || belowPage) {
                  $menu.scrollTop(offset);
                }
                $menu.removeClass(className.loading);
              }
            },
            text: function(text) {
              if(settings.action !== 'select') {
                if(settings.action == 'combo') {
                  module.debug('Changing combo button text', text, $combo);
                  if(settings.preserveHTML) {
                    $combo.html(text);
                  }
                  else {
                    $combo.text(text);
                  }
                }
                else {
                  if(text !== module.get.placeholderText()) {
                    $text.removeClass(className.placeholder);
                  }
                  module.debug('Changing text', text, $text);
                  $text
                    .removeClass(className.filtered)
                  ;
                  if(settings.preserveHTML) {
                    $text.html(text);
                  }
                  else {
                    $text.text(text);
                  }
                }
              }
            },
            selectedItem: function($item) {
              var
                value      = module.get.choiceValue($item),
                searchText = module.get.choiceText($item, false),
                text       = module.get.choiceText($item, true)
              ;
              module.debug('Setting user selection to item', $item);
              module.remove.activeItem();
              module.set.partialSearch(searchText);
              module.set.activeItem($item);
              module.set.selected(value, $item);
              module.set.text(text);
            },
            selectedLetter: function(letter) {
              var
                $selectedItem         = $item.filter('.' + className.selected),
                alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
                $nextValue            = false,
                $nextItem
              ;
              // check next of same letter
              if(alreadySelectedLetter) {
                $nextItem = $selectedItem.nextAll($item).eq(0);
                if( module.has.firstLetter($nextItem, letter) ) {
                  $nextValue  = $nextItem;
                }
              }
              // check all values
              if(!$nextValue) {
                $item
                  .each(function(){
                    if(module.has.firstLetter($(this), letter)) {
                      $nextValue = $(this);
                      return false;
                    }
                  })
                ;
              }
              // set next value
              if($nextValue) {
                module.verbose('Scrolling to next value with letter', letter);
                module.set.scrollPosition($nextValue);
                $selectedItem.removeClass(className.selected);
                $nextValue.addClass(className.selected);
                if(settings.selectOnKeydown && module.is.single()) {
                  module.set.selectedItem($nextValue);
                }
              }
            },
            direction: function($menu) {
              if(settings.direction == 'auto') {
                // reset position
                module.remove.upward();
  
                if(module.can.openDownward($menu)) {
                  module.remove.upward($menu);
                }
                else {
                  module.set.upward($menu);
                }
                if(!module.is.leftward($menu) && !module.can.openRightward($menu)) {
                  module.set.leftward($menu);
                }
              }
              else if(settings.direction == 'upward') {
                module.set.upward($menu);
              }
            },
            upward: function($currentMenu) {
              var $element = $currentMenu || $module;
              $element.addClass(className.upward);
            },
            leftward: function($currentMenu) {
              var $element = $currentMenu || $menu;
              $element.addClass(className.leftward);
            },
            value: function(value, text, $selected) {
              var
                escapedValue = module.escape.value(value),
                hasInput     = ($input.length > 0),
                currentValue = module.get.values(),
                stringValue  = (value !== undefined)
                  ? String(value)
                  : value,
                newValue
              ;
              if(hasInput) {
                if(!settings.allowReselection && stringValue == currentValue) {
                  module.verbose('Skipping value update already same value', value, currentValue);
                  if(!module.is.initialLoad()) {
                    return;
                  }
                }
  
                if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) {
                  module.debug('Adding user option', value);
                  module.add.optionValue(value);
                }
                module.debug('Updating input value', escapedValue, currentValue);
                internalChange = true;
                $input
                  .val(escapedValue)
                ;
                if(settings.fireOnInit === false && module.is.initialLoad()) {
                  module.debug('Input native change event ignored on initial load');
                }
                else {
                  module.trigger.change();
                }
                internalChange = false;
              }
              else {
                module.verbose('Storing value in metadata', escapedValue, $input);
                if(escapedValue !== currentValue) {
                  $module.data(metadata.value, stringValue);
                }
              }
              if(module.is.single() && settings.clearable) {
                // treat undefined or '' as empty
                if(!escapedValue) {
                  module.remove.clearable();
                }
                else {
                  module.set.clearable();
                }
              }
              if(settings.fireOnInit === false && module.is.initialLoad()) {
                module.verbose('No callback on initial load', settings.onChange);
              }
              else {
                settings.onChange.call(element, value, text, $selected);
              }
            },
            active: function() {
              $module
                .addClass(className.active)
              ;
            },
            multiple: function() {
              $module.addClass(className.multiple);
            },
            visible: function() {
              $module.addClass(className.visible);
            },
            exactly: function(value, $selectedItem) {
              module.debug('Setting selected to exact values');
              module.clear();
              module.set.selected(value, $selectedItem);
            },
            selected: function(value, $selectedItem) {
              var
                isMultiple = module.is.multiple(),
                $userSelectedItem
              ;
              $selectedItem = (settings.allowAdditions)
                ? $selectedItem || module.get.itemWithAdditions(value)
                : $selectedItem || module.get.item(value)
              ;
              if(!$selectedItem) {
                return;
              }
              module.debug('Setting selected menu item to', $selectedItem);
              if(module.is.multiple()) {
                module.remove.searchWidth();
              }
              if(module.is.single()) {
                module.remove.activeItem();
                module.remove.selectedItem();
              }
              else if(settings.useLabels) {
                module.remove.selectedItem();
              }
              // select each item
              $selectedItem
                .each(function() {
                  var
                    $selected      = $(this),
                    selectedText   = module.get.choiceText($selected),
                    selectedValue  = module.get.choiceValue($selected, selectedText),
  
                    isFiltered     = $selected.hasClass(className.filtered),
                    isActive       = $selected.hasClass(className.active),
                    isUserValue    = $selected.hasClass(className.addition),
                    shouldAnimate  = (isMultiple && $selectedItem.length == 1)
                  ;
                  if(isMultiple) {
                    if(!isActive || isUserValue) {
                      if(settings.apiSettings && settings.saveRemoteData) {
                        module.save.remoteData(selectedText, selectedValue);
                      }
                      if(settings.useLabels) {
                        module.add.label(selectedValue, selectedText, shouldAnimate);
                        module.add.value(selectedValue, selectedText, $selected);
                        module.set.activeItem($selected);
                        module.filterActive();
                        module.select.nextAvailable($selectedItem);
                      }
                      else {
                        module.add.value(selectedValue, selectedText, $selected);
                        module.set.text(module.add.variables(message.count));
                        module.set.activeItem($selected);
                      }
                    }
                    else if(!isFiltered) {
                      module.debug('Selected active value, removing label');
                      module.remove.selected(selectedValue);
                    }
                  }
                  else {
                    if(settings.apiSettings && settings.saveRemoteData) {
                      module.save.remoteData(selectedText, selectedValue);
                    }
                    module.set.text(selectedText);
                    module.set.value(selectedValue, selectedText, $selected);
                    $selected
                      .addClass(className.active)
                      .addClass(className.selected)
                    ;
                  }
                })
              ;
            },
            clearable: function() {
              $icon.addClass(className.clear);
            },
          },
  
          add: {
            label: function(value, text, shouldAnimate) {
              var
                $next  = module.is.searchSelection()
                  ? $search
                  : $text,
                escapedValue = module.escape.value(value),
                $label
              ;
              if(settings.ignoreCase) {
                escapedValue = escapedValue.toLowerCase();
              }
              $label =  $('<a />')
                .addClass(className.label)
                .attr('data-' + metadata.value, escapedValue)
                .html(templates.label(escapedValue, text))
              ;
              $label = settings.onLabelCreate.call($label, escapedValue, text);
  
              if(module.has.label(value)) {
                module.debug('User selection already exists, skipping', escapedValue);
                return;
              }
              if(settings.label.variation) {
                $label.addClass(settings.label.variation);
              }
              if(shouldAnimate === true) {
                module.debug('Animating in label', $label);
                $label
                  .addClass(className.hidden)
                  .insertBefore($next)
                  .transition(settings.label.transition, settings.label.duration)
                ;
              }
              else {
                module.debug('Adding selection label', $label);
                $label
                  .insertBefore($next)
                ;
              }
            },
            message: function(message) {
              var
                $message = $menu.children(selector.message),
                html     = settings.templates.message(module.add.variables(message))
              ;
              if($message.length > 0) {
                $message
                  .html(html)
                ;
              }
              else {
                $message = $('<div/>')
                  .html(html)
                  .addClass(className.message)
                  .appendTo($menu)
                ;
              }
            },
            optionValue: function(value) {
              var
                escapedValue = module.escape.value(value),
                $option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
                hasOption    = ($option.length > 0)
              ;
              if(hasOption) {
                return;
              }
              // temporarily disconnect observer
              module.disconnect.selectObserver();
              if( module.is.single() ) {
                module.verbose('Removing previous user addition');
                $input.find('option.' + className.addition).remove();
              }
              $('<option/>')
                .prop('value', escapedValue)
                .addClass(className.addition)
                .html(value)
                .appendTo($input)
              ;
              module.verbose('Adding user addition as an <option>', value);
              module.observe.select();
            },
            userSuggestion: function(value) {
              var
                $addition         = $menu.children(selector.addition),
                $existingItem     = module.get.item(value),
                alreadyHasValue   = $existingItem && $existingItem.not(selector.addition).length,
                hasUserSuggestion = $addition.length > 0,
                html
              ;
              if(settings.useLabels && module.has.maxSelections()) {
                return;
              }
              if(value === '' || alreadyHasValue) {
                $addition.remove();
                return;
              }
              if(hasUserSuggestion) {
                $addition
                  .data(metadata.value, value)
                  .data(metadata.text, value)
                  .attr('data-' + metadata.value, value)
                  .attr('data-' + metadata.text, value)
                  .removeClass(className.filtered)
                ;
                if(!settings.hideAdditions) {
                  html = settings.templates.addition( module.add.variables(message.addResult, value) );
                  $addition
                    .html(html)
                  ;
                }
                module.verbose('Replacing user suggestion with new value', $addition);
              }
              else {
                $addition = module.create.userChoice(value);
                $addition
                  .prependTo($menu)
                ;
                module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
              }
              if(!settings.hideAdditions || module.is.allFiltered()) {
                $addition
                  .addClass(className.selected)
                  .siblings()
                  .removeClass(className.selected)
                ;
              }
              module.refreshItems();
            },
            variables: function(message, term) {
              var
                hasCount    = (message.search('{count}') !== -1),
                hasMaxCount = (message.search('{maxCount}') !== -1),
                hasTerm     = (message.search('{term}') !== -1),
                values,
                count,
                query
              ;
              module.verbose('Adding templated variables to message', message);
              if(hasCount) {
                count  = module.get.selectionCount();
                message = message.replace('{count}', count);
              }
              if(hasMaxCount) {
                count  = module.get.selectionCount();
                message = message.replace('{maxCount}', settings.maxSelections);
              }
              if(hasTerm) {
                query   = term || module.get.query();
                message = message.replace('{term}', query);
              }
              return message;
            },
            value: function(addedValue, addedText, $selectedItem) {
              var
                currentValue = module.get.values(),
                newValue
              ;
              if(module.has.value(addedValue)) {
                module.debug('Value already selected');
                return;
              }
              if(addedValue === '') {
                module.debug('Cannot select blank values from multiselect');
                return;
              }
              // extend current array
              if($.isArray(currentValue)) {
                newValue = currentValue.concat([addedValue]);
                newValue = module.get.uniqueArray(newValue);
              }
              else {
                newValue = [addedValue];
              }
              // add values
              if( module.has.selectInput() ) {
                if(module.can.extendSelect()) {
                  module.debug('Adding value to select', addedValue, newValue, $input);
                  module.add.optionValue(addedValue);
                }
              }
              else {
                newValue = newValue.join(settings.delimiter);
                module.debug('Setting hidden input to delimited value', newValue, $input);
              }
  
              if(settings.fireOnInit === false && module.is.initialLoad()) {
                module.verbose('Skipping onadd callback on initial load', settings.onAdd);
              }
              else {
                settings.onAdd.call(element, addedValue, addedText, $selectedItem);
              }
              module.set.value(newValue, addedValue, addedText, $selectedItem);
              module.check.maxSelections();
            },
          },
  
          remove: {
            active: function() {
              $module.removeClass(className.active);
            },
            activeLabel: function() {
              $module.find(selector.label).removeClass(className.active);
            },
            empty: function() {
              $module.removeClass(className.empty);
            },
            loading: function() {
              $module.removeClass(className.loading);
            },
            initialLoad: function() {
              initialLoad = false;
            },
            upward: function($currentMenu) {
              var $element = $currentMenu || $module;
              $element.removeClass(className.upward);
            },
            leftward: function($currentMenu) {
              var $element = $currentMenu || $menu;
              $element.removeClass(className.leftward);
            },
            visible: function() {
              $module.removeClass(className.visible);
            },
            activeItem: function() {
              $item.removeClass(className.active);
            },
            filteredItem: function() {
              if(settings.useLabels && module.has.maxSelections() ) {
                return;
              }
              if(settings.useLabels && module.is.multiple()) {
                $item.not('.' + className.active).removeClass(className.filtered);
              }
              else {
                $item.removeClass(className.filtered);
              }
              module.remove.empty();
            },
            optionValue: function(value) {
              var
                escapedValue = module.escape.value(value),
                $option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
                hasOption    = ($option.length > 0)
              ;
              if(!hasOption || !$option.hasClass(className.addition)) {
                return;
              }
              // temporarily disconnect observer
              if(selectObserver) {
                selectObserver.disconnect();
                module.verbose('Temporarily disconnecting mutation observer');
              }
              $option.remove();
              module.verbose('Removing user addition as an <option>', escapedValue);
              if(selectObserver) {
                selectObserver.observe($input[0], {
                  childList : true,
                  subtree   : true
                });
              }
            },
            message: function() {
              $menu.children(selector.message).remove();
            },
            searchWidth: function() {
              $search.css('width', '');
            },
            searchTerm: function() {
              module.verbose('Cleared search term');
              $search.val('');
              module.set.filtered();
            },
            userAddition: function() {
              $item.filter(selector.addition).remove();
            },
            selected: function(value, $selectedItem) {
              $selectedItem = (settings.allowAdditions)
                ? $selectedItem || module.get.itemWithAdditions(value)
                : $selectedItem || module.get.item(value)
              ;
  
              if(!$selectedItem) {
                return false;
              }
  
              $selectedItem
                .each(function() {
                  var
                    $selected     = $(this),
                    selectedText  = module.get.choiceText($selected),
                    selectedValue = module.get.choiceValue($selected, selectedText)
                  ;
                  if(module.is.multiple()) {
                    if(settings.useLabels) {
                      module.remove.value(selectedValue, selectedText, $selected);
                      module.remove.label(selectedValue);
                    }
                    else {
                      module.remove.value(selectedValue, selectedText, $selected);
                      if(module.get.selectionCount() === 0) {
                        module.set.placeholderText();
                      }
                      else {
                        module.set.text(module.add.variables(message.count));
                      }
                    }
                  }
                  else {
                    module.remove.value(selectedValue, selectedText, $selected);
                  }
                  $selected
                    .removeClass(className.filtered)
                    .removeClass(className.active)
                  ;
                  if(settings.useLabels) {
                    $selected.removeClass(className.selected);
                  }
                })
              ;
            },
            selectedItem: function() {
              $item.removeClass(className.selected);
            },
            value: function(removedValue, removedText, $removedItem) {
              var
                values = module.get.values(),
                newValue
              ;
              if( module.has.selectInput() ) {
                module.verbose('Input is <select> removing selected option', removedValue);
                newValue = module.remove.arrayValue(removedValue, values);
                module.remove.optionValue(removedValue);
              }
              else {
                module.verbose('Removing from delimited values', removedValue);
                newValue = module.remove.arrayValue(removedValue, values);
                newValue = newValue.join(settings.delimiter);
              }
              if(settings.fireOnInit === false && module.is.initialLoad()) {
                module.verbose('No callback on initial load', settings.onRemove);
              }
              else {
                settings.onRemove.call(element, removedValue, removedText, $removedItem);
              }
              module.set.value(newValue, removedText, $removedItem);
              module.check.maxSelections();
            },
            arrayValue: function(removedValue, values) {
              if( !$.isArray(values) ) {
                values = [values];
              }
              values = $.grep(values, function(value){
                return (removedValue != value);
              });
              module.verbose('Removed value from delimited string', removedValue, values);
              return values;
            },
            label: function(value, shouldAnimate) {
              var
                $labels       = $module.find(selector.label),
                $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(value) +'"]')
              ;
              module.verbose('Removing label', $removedLabel);
              $removedLabel.remove();
            },
            activeLabels: function($activeLabels) {
              $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active);
              module.verbose('Removing active label selections', $activeLabels);
              module.remove.labels($activeLabels);
            },
            labels: function($labels) {
              $labels = $labels || $module.find(selector.label);
              module.verbose('Removing labels', $labels);
              $labels
                .each(function(){
                  var
                    $label      = $(this),
                    value       = $label.data(metadata.value),
                    stringValue = (value !== undefined)
                      ? String(value)
                      : value,
                    isUserValue = module.is.userValue(stringValue)
                  ;
                  if(settings.onLabelRemove.call($label, value) === false) {
                    module.debug('Label remove callback cancelled removal');
                    return;
                  }
                  module.remove.message();
                  if(isUserValue) {
                    module.remove.value(stringValue);
                    module.remove.label(stringValue);
                  }
                  else {
                    // selected will also remove label
                    module.remove.selected(stringValue);
                  }
                })
              ;
            },
            tabbable: function() {
              if( module.is.searchSelection() ) {
                module.debug('Searchable dropdown initialized');
                $search
                  .removeAttr('tabindex')
                ;
                $menu
                  .removeAttr('tabindex')
                ;
              }
              else {
                module.debug('Simple selection dropdown initialized');
                $module
                  .removeAttr('tabindex')
                ;
                $menu
                  .removeAttr('tabindex')
                ;
              }
            },
            clearable: function() {
              $icon.removeClass(className.clear);
            }
          },
  
          has: {
            menuSearch: function() {
              return (module.has.search() && $search.closest($menu).length > 0);
            },
            search: function() {
              return ($search.length > 0);
            },
            sizer: function() {
              return ($sizer.length > 0);
            },
            selectInput: function() {
              return ( $input.is('select') );
            },
            minCharacters: function(searchTerm) {
              if(settings.minCharacters) {
                searchTerm = (searchTerm !== undefined)
                  ? String(searchTerm)
                  : String(module.get.query())
                ;
                return (searchTerm.length >= settings.minCharacters);
              }
              return true;
            },
            firstLetter: function($item, letter) {
              var
                text,
                firstLetter
              ;
              if(!$item || $item.length === 0 || typeof letter !== 'string') {
                return false;
              }
              text        = module.get.choiceText($item, false);
              letter      = letter.toLowerCase();
              firstLetter = String(text).charAt(0).toLowerCase();
              return (letter == firstLetter);
            },
            input: function() {
              return ($input.length > 0);
            },
            items: function() {
              return ($item.length > 0);
            },
            menu: function() {
              return ($menu.length > 0);
            },
            message: function() {
              return ($menu.children(selector.message).length !== 0);
            },
            label: function(value) {
              var
                escapedValue = module.escape.value(value),
                $labels      = $module.find(selector.label)
              ;
              if(settings.ignoreCase) {
                escapedValue = escapedValue.toLowerCase();
              }
              return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0);
            },
            maxSelections: function() {
              return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
            },
            allResultsFiltered: function() {
              var
                $normalResults = $item.not(selector.addition)
              ;
              return ($normalResults.filter(selector.unselectable).length === $normalResults.length);
            },
            userSuggestion: function() {
              return ($menu.children(selector.addition).length > 0);
            },
            query: function() {
              return (module.get.query() !== '');
            },
            value: function(value) {
              return (settings.ignoreCase)
                ? module.has.valueIgnoringCase(value)
                : module.has.valueMatchingCase(value)
              ;
            },
            valueMatchingCase: function(value) {
              var
                values   = module.get.values(),
                hasValue = $.isArray(values)
                 ? values && ($.inArray(value, values) !== -1)
                 : (values == value)
              ;
              return (hasValue)
                ? true
                : false
              ;
            },
            valueIgnoringCase: function(value) {
              var
                values   = module.get.values(),
                hasValue = false
              ;
              if(!$.isArray(values)) {
                values = [values];
              }
              $.each(values, function(index, existingValue) {
                if(String(value).toLowerCase() == String(existingValue).toLowerCase()) {
                  hasValue = true;
                  return false;
                }
              });
              return hasValue;
            }
          },
  
          is: {
            active: function() {
              return $module.hasClass(className.active);
            },
            animatingInward: function() {
              return $menu.transition('is inward');
            },
            animatingOutward: function() {
              return $menu.transition('is outward');
            },
            bubbledLabelClick: function(event) {
              return $(event.target).is('select, input') && $module.closest('label').length > 0;
            },
            bubbledIconClick: function(event) {
              return $(event.target).closest($icon).length > 0;
            },
            alreadySetup: function() {
              return ($module.is('select') && $module.parent(selector.dropdown).data(moduleNamespace) !== undefined && $module.prev().length === 0);
            },
            animating: function($subMenu) {
              return ($subMenu)
                ? $subMenu.transition && $subMenu.transition('is animating')
                : $menu.transition    && $menu.transition('is animating')
              ;
            },
            leftward: function($subMenu) {
              var $selectedMenu = $subMenu || $menu;
              return $selectedMenu.hasClass(className.leftward);
            },
            disabled: function() {
              return $module.hasClass(className.disabled);
            },
            focused: function() {
              return (document.activeElement === $module[0]);
            },
            focusedOnSearch: function() {
              return (document.activeElement === $search[0]);
            },
            allFiltered: function() {
              return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() );
            },
            hidden: function($subMenu) {
              return !module.is.visible($subMenu);
            },
            initialLoad: function() {
              return initialLoad;
            },
            inObject: function(needle, object) {
              var
                found = false
              ;
              $.each(object, function(index, property) {
                if(property == needle) {
                  found = true;
                  return true;
                }
              });
              return found;
            },
            multiple: function() {
              return $module.hasClass(className.multiple);
            },
            remote: function() {
              return settings.apiSettings && module.can.useAPI();
            },
            single: function() {
              return !module.is.multiple();
            },
            selectMutation: function(mutations) {
              var
                selectChanged = false
              ;
              $.each(mutations, function(index, mutation) {
                if(mutation.target && $(mutation.target).is('select')) {
                  selectChanged = true;
                  return true;
                }
              });
              return selectChanged;
            },
            search: function() {
              return $module.hasClass(className.search);
            },
            searchSelection: function() {
              return ( module.has.search() && $search.parent(selector.dropdown).length === 1 );
            },
            selection: function() {
              return $module.hasClass(className.selection);
            },
            userValue: function(value) {
              return ($.inArray(value, module.get.userValues()) !== -1);
            },
            upward: function($menu) {
              var $element = $menu || $module;
              return $element.hasClass(className.upward);
            },
            visible: function($subMenu) {
              return ($subMenu)
                ? $subMenu.hasClass(className.visible)
                : $menu.hasClass(className.visible)
              ;
            },
            verticallyScrollableContext: function() {
              var
                overflowY = ($context.get(0) !== window)
                  ? $context.css('overflow-y')
                  : false
              ;
              return (overflowY == 'auto' || overflowY == 'scroll');
            },
            horizontallyScrollableContext: function() {
              var
                overflowX = ($context.get(0) !== window)
                  ? $context.css('overflow-X')
                  : false
              ;
              return (overflowX == 'auto' || overflowX == 'scroll');
            }
          },
  
          can: {
            activate: function($item) {
              if(settings.useLabels) {
                return true;
              }
              if(!module.has.maxSelections()) {
                return true;
              }
              if(module.has.maxSelections() && $item.hasClass(className.active)) {
                return true;
              }
              return false;
            },
            openDownward: function($subMenu) {
              var
                $currentMenu    = $subMenu || $menu,
                canOpenDownward = true,
                onScreen        = {},
                calculations
              ;
              $currentMenu
                .addClass(className.loading)
              ;
              calculations = {
                context: {
                  offset    : ($context.get(0) === window)
                    ? { top: 0, left: 0}
                    : $context.offset(),
                  scrollTop : $context.scrollTop(),
                  height    : $context.outerHeight()
                },
                menu : {
                  offset: $currentMenu.offset(),
                  height: $currentMenu.outerHeight()
                }
              };
              if(module.is.verticallyScrollableContext()) {
                calculations.menu.offset.top += calculations.context.scrollTop;
              }
              onScreen = {
                above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.context.offset.top - calculations.menu.height,
                below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top - calculations.context.offset.top + calculations.menu.height
              };
              if(onScreen.below) {
                module.verbose('Dropdown can fit in context downward', onScreen);
                canOpenDownward = true;
              }
              else if(!onScreen.below && !onScreen.above) {
                module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen);
                canOpenDownward = true;
              }
              else {
                module.verbose('Dropdown cannot fit below, opening upward', onScreen);
                canOpenDownward = false;
              }
              $currentMenu.removeClass(className.loading);
              return canOpenDownward;
            },
            openRightward: function($subMenu) {
              var
                $currentMenu     = $subMenu || $menu,
                canOpenRightward = true,
                isOffscreenRight = false,
                calculations
              ;
              $currentMenu
                .addClass(className.loading)
              ;
              calculations = {
                context: {
                  offset     : ($context.get(0) === window)
                    ? { top: 0, left: 0}
                    : $context.offset(),
                  scrollLeft : $context.scrollLeft(),
                  width      : $context.outerWidth()
                },
                menu: {
                  offset : $currentMenu.offset(),
                  width  : $currentMenu.outerWidth()
                }
              };
              if(module.is.horizontallyScrollableContext()) {
                calculations.menu.offset.left += calculations.context.scrollLeft;
              }
              isOffscreenRight = (calculations.menu.offset.left - calculations.context.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width);
              if(isOffscreenRight) {
                module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight);
                canOpenRightward = false;
              }
              $currentMenu.removeClass(className.loading);
              return canOpenRightward;
            },
            click: function() {
              return (hasTouch || settings.on == 'click');
            },
            extendSelect: function() {
              return settings.allowAdditions || settings.apiSettings;
            },
            show: function() {
              return !module.is.disabled() && (module.has.items() || module.has.message());
            },
            useAPI: function() {
              return $.fn.api !== undefined;
            }
          },
  
          animate: {
            show: function(callback, $subMenu) {
              var
                $currentMenu = $subMenu || $menu,
                start = ($subMenu)
                  ? function() {}
                  : function() {
                    module.hideSubMenus();
                    module.hideOthers();
                    module.set.active();
                  },
                transition
              ;
              callback = $.isFunction(callback)
                ? callback
                : function(){}
              ;
              module.verbose('Doing menu show animation', $currentMenu);
              module.set.direction($subMenu);
              transition = module.get.transition($subMenu);
              if( module.is.selection() ) {
                module.set.scrollPosition(module.get.selectedItem(), true);
              }
              if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
                if(transition == 'none') {
                  start();
                  $currentMenu.transition('show');
                  callback.call(element);
                }
                else if($.fn.transition !== undefined && $module.transition('is supported')) {
                  $currentMenu
                    .transition({
                      animation  : transition + ' in',
                      debug      : settings.debug,
                      verbose    : settings.verbose,
                      duration   : settings.duration,
                      queue      : true,
                      onStart    : start,
                      onComplete : function() {
                        callback.call(element);
                      }
                    })
                  ;
                }
                else {
                  module.error(error.noTransition, transition);
                }
              }
            },
            hide: function(callback, $subMenu) {
              var
                $currentMenu = $subMenu || $menu,
                duration = ($subMenu)
                  ? (settings.duration * 0.9)
                  : settings.duration,
                start = ($subMenu)
                  ? function() {}
                  : function() {
                    if( module.can.click() ) {
                      module.unbind.intent();
                    }
                    module.remove.active();
                  },
                transition = module.get.transition($subMenu)
              ;
              callback = $.isFunction(callback)
                ? callback
                : function(){}
              ;
              if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
                module.verbose('Doing menu hide animation', $currentMenu);
  
                if(transition == 'none') {
                  start();
                  $currentMenu.transition('hide');
                  callback.call(element);
                }
                else if($.fn.transition !== undefined && $module.transition('is supported')) {
                  $currentMenu
                    .transition({
                      animation  : transition + ' out',
                      duration   : settings.duration,
                      debug      : settings.debug,
                      verbose    : settings.verbose,
                      queue      : false,
                      onStart    : start,
                      onComplete : function() {
                        callback.call(element);
                      }
                    })
                  ;
                }
                else {
                  module.error(error.transition);
                }
              }
            }
          },
  
          hideAndClear: function() {
            module.remove.searchTerm();
            if( module.has.maxSelections() ) {
              return;
            }
            if(module.has.search()) {
              module.hide(function() {
                module.remove.filteredItem();
              });
            }
            else {
              module.hide();
            }
          },
  
          delay: {
            show: function() {
              module.verbose('Delaying show event to ensure user intent');
              clearTimeout(module.timer);
              module.timer = setTimeout(module.show, settings.delay.show);
            },
            hide: function() {
              module.verbose('Delaying hide event to ensure user intent');
              clearTimeout(module.timer);
              module.timer = setTimeout(module.hide, settings.delay.hide);
            }
          },
  
          escape: {
            value: function(value) {
              var
                multipleValues = $.isArray(value),
                stringValue    = (typeof value === 'string'),
                isUnparsable   = (!stringValue && !multipleValues),
                hasQuotes      = (stringValue && value.search(regExp.quote) !== -1),
                values         = []
              ;
              if(isUnparsable || !hasQuotes) {
                return value;
              }
              module.debug('Encoding quote values for use in select', value);
              if(multipleValues) {
                $.each(value, function(index, value){
                  values.push(value.replace(regExp.quote, '&quot;'));
                });
                return values;
              }
              return value.replace(regExp.quote, '&quot;');
            },
            string: function(text) {
              text =  String(text);
              return text.replace(regExp.escape, '\\$&');
            }
          },
  
          setting: function(name, value) {
            module.debug('Changing setting', name, value);
            if( $.isPlainObject(name) ) {
              $.extend(true, settings, name);
            }
            else if(value !== undefined) {
              if($.isPlainObject(settings[name])) {
                $.extend(true, settings[name], value);
              }
              else {
                settings[name] = value;
              }
            }
            else {
              return settings[name];
            }
          },
          internal: function(name, value) {
            if( $.isPlainObject(name) ) {
              $.extend(true, module, name);
            }
            else if(value !== undefined) {
              module[name] = value;
            }
            else {
              return module[name];
            }
          },
          debug: function() {
            if(!settings.silent && settings.debug) {
              if(settings.performance) {
                module.performance.log(arguments);
              }
              else {
                module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
                module.debug.apply(console, arguments);
              }
            }
          },
          verbose: function() {
            if(!settings.silent && settings.verbose && settings.debug) {
              if(settings.performance) {
                module.performance.log(arguments);
              }
              else {
                module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
                module.verbose.apply(console, arguments);
              }
            }
          },
          error: function() {
            if(!settings.silent) {
              module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
              module.error.apply(console, arguments);
            }
          },
          performance: {
            log: function(message) {
              var
                currentTime,
                executionTime,
                previousTime
              ;
              if(settings.performance) {
                currentTime   = new Date().getTime();
                previousTime  = time || currentTime;
                executionTime = currentTime - previousTime;
                time          = currentTime;
                performance.push({
                  'Name'           : message[0],
                  'Arguments'      : [].slice.call(message, 1) || '',
                  'Element'        : element,
                  'Execution Time' : executionTime
                });
              }
              clearTimeout(module.performance.timer);
              module.performance.timer = setTimeout(module.performance.display, 500);
            },
            display: function() {
              var
                title = settings.name + ':',
                totalTime = 0
              ;
              time = false;
              clearTimeout(module.performance.timer);
              $.each(performance, function(index, data) {
                totalTime += data['Execution Time'];
              });
              title += ' ' + totalTime + 'ms';
              if(moduleSelector) {
                title += ' \'' + moduleSelector + '\'';
              }
              if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
                console.groupCollapsed(title);
                if(console.table) {
                  console.table(performance);
                }
                else {
                  $.each(performance, function(index, data) {
                    console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
                  });
                }
                console.groupEnd();
              }
              performance = [];
            }
          },
          invoke: function(query, passedArguments, context) {
            var
              object = instance,
              maxDepth,
              found,
              response
            ;
            passedArguments = passedArguments || queryArguments;
            context         = element         || context;
            if(typeof query == 'string' && object !== undefined) {
              query    = query.split(/[\. ]/);
              maxDepth = query.length - 1;
              $.each(query, function(depth, value) {
                var camelCaseValue = (depth != maxDepth)
                  ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                  : query
                ;
                if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
                  object = object[camelCaseValue];
                }
                else if( object[camelCaseValue] !== undefined ) {
                  found = object[camelCaseValue];
                  return false;
                }
                else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
                  object = object[value];
                }
                else if( object[value] !== undefined ) {
                  found = object[value];
                  return false;
                }
                else {
                  module.error(error.method, query);
                  return false;
                }
              });
            }
            if ( $.isFunction( found ) ) {
              response = found.apply(context, passedArguments);
            }
            else if(found !== undefined) {
              response = found;
            }
            if($.isArray(returnedValue)) {
              returnedValue.push(response);
            }
            else if(returnedValue !== undefined) {
              returnedValue = [returnedValue, response];
            }
            else if(response !== undefined) {
              returnedValue = response;
            }
            return found;
          }
        };
  
        if(methodInvoked) {
          if(instance === undefined) {
            module.initialize();
          }
          module.invoke(query);
        }
        else {
          if(instance !== undefined) {
            instance.invoke('destroy');
          }
          module.initialize();
        }
      })
    ;
    return (returnedValue !== undefined)
      ? returnedValue
      : $allModules
    ;
  };
  
  $.fn.dropdown.settings = {
  
    silent                 : false,
    debug                  : false,
    verbose                : false,
    performance            : true,
  
    on                     : 'click',    // what event should show menu action on item selection
    action                 : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
  
    values                 : false,      // specify values to use for dropdown
  
    clearable              : false,      // whether the value of the dropdown can be cleared
  
    apiSettings            : false,
    selectOnKeydown        : true,       // Whether selection should occur automatically when keyboard shortcuts used
    minCharacters          : 0,          // Minimum characters required to trigger API call
  
    filterRemoteData       : false,      // Whether API results should be filtered after being returned for query term
    saveRemoteData         : true,       // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh
  
    throttle               : 200,        // How long to wait after last user input to search remotely
  
    context                : window,     // Context to use when determining if on screen
    direction              : 'auto',     // Whether dropdown should always open in one direction
    keepOnScreen           : true,       // Whether dropdown should check whether it is on screen before showing
  
    match                  : 'both',     // what to match against with search selection (both, text, or label)
    fullTextSearch         : false,      // search anywhere in value (set to 'exact' to require exact matches)
  
    placeholder            : 'auto',     // whether to convert blank <select> values to placeholder text
    preserveHTML           : true,       // preserve html when selecting value
    sortSelect             : false,      // sort selection on init
  
    forceSelection         : true,       // force a choice on blur with search selection
  
    allowAdditions         : false,      // whether multiple select should allow user added values
    ignoreCase             : false,       // whether to consider values not matching in case to be the same
    hideAdditions          : true,       // whether or not to hide special message prompting a user they can enter a value
  
    maxSelections          : false,      // When set to a number limits the number of selections to this count
    useLabels              : true,       // whether multiple select should filter currently active selections from choices
    delimiter              : ',',        // when multiselect uses normal <input> the values will be delimited with this character
  
    showOnFocus            : true,       // show menu on focus
    allowReselection       : false,      // whether current value should trigger callbacks when reselected
    allowTab               : true,       // add tabindex to element
    allowCategorySelection : false,      // allow elements with sub-menus to be selected
  
    fireOnInit             : false,      // Whether callbacks should fire when initializing dropdown values
  
    transition             : 'auto',     // auto transition will slide down or up based on direction
    duration               : 200,        // duration of transition
  
    glyphWidth             : 1.037,      // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width
  
    // label settings on multi-select
    label: {
      transition : 'scale',
      duration   : 200,
      variation  : false
    },
  
    // delay before event
    delay : {
      hide   : 300,
      show   : 200,
      search : 20,
      touch  : 50
    },
  
    /* Callbacks */
    onChange      : function(value, text, $selected){},
    onAdd         : function(value, text, $selected){},
    onRemove      : function(value, text, $selected){},
  
    onLabelSelect : function($selectedLabels){},
    onLabelCreate : function(value, text) { return $(this); },
    onLabelRemove : function(value) { return true; },
    onNoResults   : function(searchTerm) { return true; },
    onShow        : function(){},
    onHide        : function(){},
  
    /* Component */
    name           : 'Dropdown',
    namespace      : 'dropdown',
  
    message: {
      addResult     : 'Add <b>{term}</b>',
      count         : '{count} selected',
      maxSelections : 'Max {maxCount} selections',
      noResults     : 'No results found.',
      serverError   : 'There was an error contacting the server'
    },
  
    error : {
      action          : 'You called a dropdown action that was not defined',
      alreadySetup    : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
      labels          : 'Allowing user additions currently requires the use of labels.',
      missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values',
      method          : 'The method you called is not defined.',
      noAPI           : 'The API module is required to load resources remotely',
      noStorage       : 'Saving remote data requires session storage',
      noTransition    : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>'
    },
  
    regExp : {
      escape   : /[-[\]{}()*+?.,\\^$|#\s]/g,
      quote    : /"/g
    },
  
    metadata : {
      defaultText     : 'defaultText',
      defaultValue    : 'defaultValue',
      placeholderText : 'placeholder',
      text            : 'text',
      value           : 'value'
    },
  
    // property names for remote query
    fields: {
      remoteValues : 'results',  // grouping for api results
      values       : 'values',   // grouping for all dropdown values
      disabled     : 'disabled', // whether value should be disabled
      name         : 'name',     // displayed dropdown text
      value        : 'value',    // actual dropdown value
      text         : 'text'      // displayed text when selected
    },
  
    keys : {
      backspace  : 8,
      delimiter  : 188, // comma
      deleteKey  : 46,
      enter      : 13,
      escape     : 27,
      pageUp     : 33,
      pageDown   : 34,
      leftArrow  : 37,
      upArrow    : 38,
      rightArrow : 39,
      downArrow  : 40
    },
  
    selector : {
      addition     : '.addition',
      dropdown     : '.ui.dropdown',
      hidden       : '.hidden',
      icon         : '> .dropdown.icon',
      input        : '> input[type="hidden"], > select',
      item         : '.item',
      label        : '> .label',
      remove       : '> .label > .delete.icon',
      siblingLabel : '.label',
      menu         : '.menu',
      message      : '.message',
      menuIcon     : '.dropdown.icon',
      search       : 'input.search, .menu > .search > input, .menu input.search',
      sizer        : '> input.sizer',
      text         : '> .text:not(.icon)',
      unselectable : '.disabled, .filtered'
    },
  
    className : {
      active      : 'active',
      addition    : 'addition',
      animating   : 'animating',
      clear       : 'clear',
      disabled    : 'disabled',
      empty       : 'empty',
      dropdown    : 'ui dropdown',
      filtered    : 'filtered',
      hidden      : 'hidden transition',
      item        : 'item',
      label       : 'ui label',
      loading     : 'loading',
      menu        : 'menu',
      message     : 'message',
      multiple    : 'multiple',
      placeholder : 'default',
      sizer       : 'sizer',
      search      : 'search',
      selected    : 'selected',
      selection   : 'selection',
      upward      : 'upward',
      leftward    : 'left',
      visible     : 'visible'
    }
  
  };
  
  /* Templates */
  $.fn.dropdown.settings.templates = {
  
    // generates dropdown from select values
    dropdown: function(select) {
      var
        placeholder = select.placeholder || false,
        values      = select.values || {},
        html        = ''
      ;
      html +=  '<i class="dropdown icon"></i>';
      if(select.placeholder) {
        html += '<div class="default text">' + placeholder + '</div>';
      }
      else {
        html += '<div class="text"></div>';
      }
      html += '<div class="menu">';
      $.each(select.values, function(index, option) {
        html += (option.disabled)
          ? '<div class="disabled item" data-value="' + option.value + '">' + option.name + '</div>'
          : '<div class="item" data-value="' + option.value + '">' + option.name + '</div>'
        ;
      });
      html += '</div>';
      return html;
    },
  
    // generates just menu from select
    menu: function(response, fields) {
      var
        values = response[fields.values] || {},
        html   = ''
      ;
      $.each(values, function(index, option) {
        var
          maybeText = (option[fields.text])
            ? 'data-text="' + option[fields.text] + '"'
            : '',
          maybeDisabled = (option[fields.disabled])
            ? 'disabled '
            : ''
        ;
        html += '<div class="'+ maybeDisabled +'item" data-value="' + option[fields.value] + '"' + maybeText + '>';
        html +=   option[fields.name];
        html += '</div>';
      });
      return html;
    },
  
    // generates label for multiselect
    label: function(value, text) {
      return text + '<i class="delete icon"></i>';
    },
  
  
    // generates messages like "No results"
    message: function(message) {
      return message;
    },
  
    // generates user addition to selection menu
    addition: function(choice) {
      return choice;
    }
  
  };
  
  })( jQuery, window, document );
  